// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of protoc;

/// Generates the Dart enum corresponding to a oneof declaration.
///
/// The enum is used to represent the state of a oneof when using the
/// corresponding which-method.
class OneofEnumGenerator {
  static void generate(
      IndentingWriter out, String classname, List<ProtobufField> fields) {
    out.addBlock('enum ${classname} {', '}\n', () {
      for (var field in fields) {
        final name = oneofEnumMemberName(field.memberNames.fieldName);
        out.println('$name, ');
      }
      out.println('notSet');
    });
  }
}

class MessageGenerator extends ProtobufContainer {
  /// The name of the Dart class to generate.
  @override
  final String classname;

  /// The fully-qualified name of the message (without any leading '.').
  @override
  final String fullName;

  /// The part of the fully qualified name that comes after the package prefix.
  ///
  /// For nested messages this will include the names of the parents.
  ///
  /// For example:
  /// ```
  /// package foo;
  ///
  /// message Container {
  ///   message Nested {
  ///     int32 int32_value = 1;
  ///   }
  /// }
  /// ```
  /// The nested message will have a `fullName` of 'foo.Container.Nested', and a
  /// `messageName` of 'Container.Nested'.
  String get messageName =>
      fullName.substring(package.isEmpty ? 0 : package.length + 1);

  PbMixin mixin;

  final ProtobufContainer _parent;
  final DescriptorProto _descriptor;
  final List<EnumGenerator> _enumGenerators = <EnumGenerator>[];
  final List<MessageGenerator> _messageGenerators = <MessageGenerator>[];
  final List<ExtensionGenerator> _extensionGenerators = <ExtensionGenerator>[];

  /// Stores the list of fields belonging to each oneof declaration identified
  /// by the index in the containing types's oneof_decl list.
  /// Only contains the 'real' oneofs.
  final List<List<ProtobufField>> _oneofFields;
  List<OneofNames> _oneofNames;

  List<int> _fieldPath;
  final List<int> _fieldPathSegment;

  /// See [[ProtobufContainer]
  @override
  List<int> get fieldPath =>
      _fieldPath ??= List.from(_parent.fieldPath)..addAll(_fieldPathSegment);

  // populated by resolve()
  List<ProtobufField> _fieldList;

  Set<String> _usedTopLevelNames;

  MessageGenerator._(
      DescriptorProto descriptor,
      ProtobufContainer parent,
      Map<String, PbMixin> declaredMixins,
      PbMixin defaultMixin,
      this._usedTopLevelNames,
      int repeatedFieldIndex,
      int fieldIdTag)
      : _descriptor = descriptor,
        _parent = parent,
        _fieldPathSegment = [fieldIdTag, repeatedFieldIndex],
        classname = messageOrEnumClassName(descriptor.name, _usedTopLevelNames,
            parent: parent?.classname ?? ''),
        assert(parent != null),
        fullName = parent.fullName == ''
            ? descriptor.name
            : '${parent.fullName}.${descriptor.name}',
        _oneofFields =
            List.generate(countRealOneofs(descriptor), (int index) => []) {
    mixin = _getMixin(declaredMixins, defaultMixin);
    for (var i = 0; i < _descriptor.enumType.length; i++) {
      var e = _descriptor.enumType[i];
      _enumGenerators.add(EnumGenerator.nested(e, this, _usedTopLevelNames, i));
    }

    for (var i = 0; i < _descriptor.nestedType.length; i++) {
      var n = _descriptor.nestedType[i];
      _messageGenerators.add(MessageGenerator.nested(
          n, this, declaredMixins, defaultMixin, _usedTopLevelNames, i));
    }

    // Extensions within messages won't create top-level classes and don't need
    // to check against / be added to top-level reserved names.
    final usedExtensionNames = {...forbiddenExtensionNames};
    for (var i = 0; i < _descriptor.extension.length; i++) {
      var x = _descriptor.extension[i];
      _extensionGenerators
          .add(ExtensionGenerator.nested(x, this, usedExtensionNames, i));
    }
  }

  static const _topLevelMessageTag = 4;
  static const _nestedMessageTag = 3;
  static const _messageFieldTag = 2;

  MessageGenerator.topLevel(
      DescriptorProto descriptor,
      ProtobufContainer parent,
      Map<String, PbMixin> declaredMixins,
      PbMixin defaultMixin,
      Set<String> usedNames,
      int repeatedFieldIndex)
      : this._(descriptor, parent, declaredMixins, defaultMixin, usedNames,
            repeatedFieldIndex, _topLevelMessageTag);

  MessageGenerator.nested(
      DescriptorProto descriptor,
      ProtobufContainer parent,
      Map<String, PbMixin> declaredMixins,
      PbMixin defaultMixin,
      Set<String> usedNames,
      int repeatedFieldIndex)
      : this._(descriptor, parent, declaredMixins, defaultMixin, usedNames,
            repeatedFieldIndex, _nestedMessageTag);

  @override
  String get package => _parent.package;

  /// The generator of the .pb.dart file that will declare this type.
  @override
  FileGenerator get fileGen => _parent.fileGen;

  /// Throws an exception if [resolve] hasn't been called yet.
  void checkResolved() {
    if (_fieldList == null) {
      throw StateError("message not resolved: ${fullName}");
    }
  }

  /// Returns a const expression that evaluates to the JSON for this message.
  /// [usage] represents the .pb.dart file where the expression will be used.
  String getJsonConstant(FileGenerator usage) {
    var name = "$classname\$json";
    if (usage.protoFileUri == fileGen.protoFileUri) {
      return name;
    }
    return "$fileImportPrefix.$name";
  }

  /// Adds all mixins used in this message and any submessages.
  void addMixinsTo(Set<PbMixin> output) {
    if (mixin != null) {
      output.addAll(mixin.findMixinsToApply());
    }
    for (var m in _messageGenerators) {
      m.addMixinsTo(output);
    }
  }

  // Registers message and enum types that can be used elsewhere.
  void register(GenerationContext ctx) {
    ctx.registerFieldType(this);
    for (var m in _messageGenerators) {
      m.register(ctx);
    }
    for (var e in _enumGenerators) {
      e.register(ctx);
    }
  }

  // Creates fields and resolves extension targets.
  void resolve(GenerationContext ctx) {
    if (_fieldList != null) throw StateError("message already resolved");

    var reserved = mixin?.findReservedNames() ?? const <String>[];
    var members = messageMemberNames(_descriptor, classname, _usedTopLevelNames,
        reserved: reserved);

    _fieldList = <ProtobufField>[];
    for (var names in members.fieldNames) {
      var field = ProtobufField.message(names, this, ctx);
      if (field.descriptor.hasOneofIndex() &&
          !field.descriptor.proto3Optional) {
        _oneofFields[field.descriptor.oneofIndex].add(field);
      }
      _fieldList.add(field);
    }
    _oneofNames = members.oneofNames;

    for (var m in _messageGenerators) {
      m.resolve(ctx);
    }
    for (var x in _extensionGenerators) {
      x.resolve(ctx);
    }
  }

  bool get needsFixnumImport {
    if (_fieldList == null) throw StateError("message not resolved");
    for (var field in _fieldList) {
      if (field.needsFixnumImport) return true;
    }
    for (var m in _messageGenerators) {
      if (m.needsFixnumImport) return true;
    }
    for (var x in _extensionGenerators) {
      if (x.needsFixnumImport) return true;
    }
    return false;
  }

  /// Adds dependencies of [generate] to [imports].
  ///
  /// For each .pb.dart file that the generated code needs to import,
  /// add its generator.
  void addImportsTo(
      Set<FileGenerator> imports, Set<FileGenerator> enumImports) {
    if (_fieldList == null) throw StateError("message not resolved");
    for (var field in _fieldList) {
      var typeGen = field.baseType.generator;
      if (typeGen is EnumGenerator) {
        enumImports.add(typeGen.fileGen);
      } else if (typeGen != null) {
        imports.add(typeGen.fileGen);
      }
    }
    for (var m in _messageGenerators) {
      m.addImportsTo(imports, enumImports);
    }
    for (var x in _extensionGenerators) {
      x.addImportsTo(imports, enumImports);
    }
  }

  // Returns the number of enums in this message and all nested messages.
  int get enumCount {
    var count = _enumGenerators.length;
    for (var m in _messageGenerators) {
      count += m.enumCount;
    }
    return count;
  }

  /// Adds dependencies of [generateConstants] to [imports].
  ///
  /// For each .pbjson.dart file that the generated code needs to import,
  /// add its generator.
  void addConstantImportsTo(Set<FileGenerator> imports) {
    if (_fieldList == null) throw StateError("message not resolved");
    for (var m in _messageGenerators) {
      m.addConstantImportsTo(imports);
    }
    for (var x in _extensionGenerators) {
      x.addConstantImportsTo(imports);
    }
  }

  void generate(IndentingWriter out) {
    checkResolved();

    for (var m in _messageGenerators) {
      // Don't output the generated map entry type. Instead, the `PbMap` type
      // from the protobuf library is used to hold the keys and values.
      if (m._descriptor.options.hasMapEntry()) continue;
      m.generate(out);
    }

    for (var oneof in _oneofNames) {
      OneofEnumGenerator.generate(
          out, oneof.oneofEnumName, _oneofFields[oneof.index]);
    }

    var mixinClause = '';
    if (mixin != null) {
      var mixinNames =
          mixin.findMixinsToApply().map((m) => '$_mixinImportPrefix.${m.name}');
      mixinClause = ' with ${mixinNames.join(", ")}';
    }

    final conditionalPackageName = 'const $_protobufImportPrefix.PackageName(' +
        configurationDependent('protobuf.omit_message_names', quoted(package)) +
        ')';

    var packageClause =
        package == '' ? '' : ', package: $conditionalPackageName';
    var proto3JsonClause = (mixin?.hasProto3JsonHelpers ?? false)
        ? ', toProto3Json: $_mixinImportPrefix.${mixin.name}.toProto3JsonHelper, '
            'fromProto3Json: $_mixinImportPrefix.${mixin.name}.fromProto3JsonHelper'
        : '';
    out.addAnnotatedBlock(
        'class ${classname} extends $_protobufImportPrefix.GeneratedMessage${mixinClause} {',
        '}', [
      NamedLocation(
          name: classname, fieldPathSegment: fieldPath, start: 'class '.length)
    ], () {
      for (var oneof in _oneofNames) {
        out.addBlock(
            'static const $_coreImportPrefix.Map<$_coreImportPrefix.int, ${oneof.oneofEnumName}> ${oneof.byTagMapName} = {',
            '};', () {
          for (var field in _oneofFields[oneof.index]) {
            final oneofMemberName =
                oneofEnumMemberName(field.memberNames.fieldName);
            out.println(
                '${field.number} : ${oneof.oneofEnumName}.${oneofMemberName},');
          }
          out.println('0 : ${oneof.oneofEnumName}.notSet');
        });
      }
      final conditionalMessageName = configurationDependent(
          'protobuf.omit_message_names', quoted(messageName));
      out.addBlock(
          'static final $_protobufImportPrefix.BuilderInfo _i = '
              '$_protobufImportPrefix.BuilderInfo($conditionalMessageName'
              '$packageClause'
              ', createEmptyInstance: create'
              '$proto3JsonClause)',
          ';', () {
        for (var oneof = 0; oneof < _oneofFields.length; oneof++) {
          var tags =
              _oneofFields[oneof].map((ProtobufField f) => f.number).toList();
          out.println("..oo($oneof, ${tags})");
        }

        for (var field in _fieldList) {
          out.println(field.generateBuilderInfoCall(fileGen, package));
        }

        if (_descriptor.extensionRange.isNotEmpty) {
          out.println('..hasExtensions = true');
        }
        if (!_hasRequiredFields(this, <dynamic>{})) {
          out.println('..hasRequiredFields = false');
        }
      });

      for (var x in _extensionGenerators) {
        x.generate(out);
      }

      out.println();

      out.printlnAnnotated('${classname}._() : super();', [
        NamedLocation(name: classname, fieldPathSegment: fieldPath, start: 0)
      ]);
      out.print('factory $classname(');
      if (_fieldList.isNotEmpty) {
        out.println('{');
        for (final field in _fieldList) {
          _emitDeprecatedIf(field.isDeprecated, out);
          if (field.isRepeated && !field.isMapField) {
            out.println(
                '  ${field.baseType.getRepeatedDartTypeIterable(fileGen)} ${field.memberNames.fieldName},');
          } else {
            out.println(
                '  ${field.getDartType(fileGen)} ${field.memberNames.fieldName},');
          }
        }
        out.print('}');
      }
      if (_fieldList.isNotEmpty) {
        out.println(') {');
        out.println('  final _result = create();');
        for (final field in _fieldList) {
          out.println('  if (${field.memberNames.fieldName} != null) {');
          if (field.isDeprecated) {
            out.println(
                '    // ignore: deprecated_member_use_from_same_package');
          }
          if (field.isRepeated || field.isMapField) {
            out.println(
                '    _result.${field.memberNames.fieldName}.addAll(${field.memberNames.fieldName});');
          } else {
            out.println(
                '    _result.${field.memberNames.fieldName} = ${field.memberNames.fieldName};');
          }
          out.println('  }');
        }
        out.println('  return _result;');
        out.println('}');
      } else {
        out.println(') => create();');
      }
      out.println(
          'factory ${classname}.fromBuffer($_coreImportPrefix.List<$_coreImportPrefix.int> i,'
          ' [$_protobufImportPrefix.ExtensionRegistry r = $_protobufImportPrefix.ExtensionRegistry.EMPTY])'
          ' => create()..mergeFromBuffer(i, r);');
      out.println('factory ${classname}.fromJson($_coreImportPrefix.String i,'
          ' [$_protobufImportPrefix.ExtensionRegistry r = $_protobufImportPrefix.ExtensionRegistry.EMPTY])'
          ' => create()..mergeFromJson(i, r);');
      out.println('''@$_coreImportPrefix.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version\')''');
      out.println('${classname} clone() =>'
          ' ${classname}()..mergeFromMessage(this);');
      out.println('''@$_coreImportPrefix.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version\')''');
      out.println('$classname copyWith(void Function($classname) updates) =>'
          ' super.copyWith((message) => updates(message as $classname));'
          ' // ignore: deprecated_member_use');

      out.println('$_protobufImportPrefix.BuilderInfo get info_ => _i;');

      // Factory functions which can be used as default value closures.
      out.println("@${_coreImportPrefix}.pragma('dart2js:noInline')");
      out.println('static ${classname} create() => ${classname}._();');
      out.println('${classname} createEmptyInstance() => create();');

      out.println(
          'static $_protobufImportPrefix.PbList<${classname}> createRepeated() =>'
          ' $_protobufImportPrefix.PbList<${classname}>();');
      out.println("@${_coreImportPrefix}.pragma('dart2js:noInline')");
      out.println('static ${classname} getDefault() =>'
          ' _defaultInstance ??='
          ' $_protobufImportPrefix.GeneratedMessage.\$_defaultFor<${classname}>'
          '(create);');
      out.println('static ${classname} _defaultInstance;');

      generateFieldsAccessorsMutators(out);
      mixin?.injectHelpers(out);
    });
    out.println();
  }

  // Returns true if the message type has any required fields.  If it doesn't,
  // we can optimize out calls to its isInitialized()/_findInvalidFields()
  // methods.
  //
  // already_seen is used to avoid checking the same type multiple times
  // (and also to protect against unbounded recursion).
  bool _hasRequiredFields(MessageGenerator type, Set alreadySeen) {
    if (type._fieldList == null) throw StateError("message not resolved");

    if (alreadySeen.contains(type.fullName)) {
      // The type is already in cache.  This means that either:
      // a. The type has no required fields.
      // b. We are in the midst of checking if the type has required fields,
      //    somewhere up the stack.  In this case, we know that if the type
      //    has any required fields, they'll be found when we return to it,
      //    and the whole call to HasRequiredFields() will return true.
      //    Therefore, we don't have to check if this type has required fields
      //    here.
      return false;
    }
    alreadySeen.add(type.fullName);
    // If the type has extensions, an extension with message type could contain
    // required fields, so we have to be conservative and assume such an
    // extension exists.
    if (type._descriptor.extensionRange.isNotEmpty) {
      return true;
    }

    for (var field in type._fieldList) {
      if (field.isRequired) {
        return true;
      }
      if (field.baseType.isMessage) {
        MessageGenerator child = field.baseType.generator;
        if (_hasRequiredFields(child, alreadySeen)) {
          return true;
        }
      }
    }
    return false;
  }

  void generateFieldsAccessorsMutators(IndentingWriter out) {
    _oneofNames
        .forEach((OneofNames oneof) => generateOneofAccessors(out, oneof));

    for (var field in _fieldList) {
      out.println();
      var memberFieldPath = List<int>.from(fieldPath)
        ..addAll([_messageFieldTag, field.sourcePosition]);
      generateFieldAccessorsMutators(field, out, memberFieldPath);
    }
  }

  void generateOneofAccessors(IndentingWriter out, OneofNames oneof) {
    out.println();
    out.println("${oneof.oneofEnumName} ${oneof.whichOneofMethodName}() "
        "=> ${oneof.byTagMapName}[\$_whichOneof(${oneof.index})];");
    out.println('void ${oneof.clearMethodName}() '
        '=> clearField(\$_whichOneof(${oneof.index}));');
  }

  void generateFieldAccessorsMutators(
      ProtobufField field, IndentingWriter out, List<int> memberFieldPath) {
    var fieldTypeString = field.getDartType(fileGen);
    var defaultExpr = field.getDefaultExpr();
    var names = field.memberNames;

    _emitDeprecatedIf(field.isDeprecated, out);
    _emitOverrideIf(field.overridesGetter, out);
    _emitIndexAnnotation(field.number, out);
    final getterExpr = _getterExpression(fieldTypeString, field.index,
        defaultExpr, field.isRepeated, field.isMapField);
    out.printlnAnnotated(
        '${fieldTypeString} get ${names.fieldName} => ${getterExpr};', [
      NamedLocation(
          name: names.fieldName,
          fieldPathSegment: memberFieldPath,
          start: '${fieldTypeString} get '.length)
    ]);

    if (field.isRepeated) {
      if (field.overridesSetter) {
        throw 'Field ${field.fullName} cannot override a setter for '
            '${names.fieldName} because it is repeated.';
      }
      if (field.overridesHasMethod) {
        throw 'Field ${field.fullName} cannot override '
            '${names.hasMethodName}() because it is repeated.';
      }
      if (field.overridesClearMethod) {
        throw 'Field ${field.fullName} cannot override '
            '${names.clearMethodName}() because it is repeated.';
      }
    } else {
      var fastSetter = field.baseType.setter;
      _emitDeprecatedIf(field.isDeprecated, out);
      _emitOverrideIf(field.overridesSetter, out);
      _emitIndexAnnotation(field.number, out);
      if (fastSetter != null) {
        out.printlnAnnotated(
            'set ${names.fieldName}'
            '($fieldTypeString v) { '
            '$fastSetter(${field.index}, v);'
            ' }',
            [
              NamedLocation(
                  name: names.fieldName,
                  fieldPathSegment: memberFieldPath,
                  start: 'set '.length)
            ]);
      } else {
        out.printlnAnnotated(
            'set ${names.fieldName}'
            '($fieldTypeString v) { '
            'setField(${field.number}, v);'
            ' }',
            [
              NamedLocation(
                  name: names.fieldName,
                  fieldPathSegment: memberFieldPath,
                  start: 'set '.length)
            ]);
      }
      if (field.hasPresence) {
        _emitDeprecatedIf(field.isDeprecated, out);
        _emitOverrideIf(field.overridesHasMethod, out);
        _emitIndexAnnotation(field.number, out);
        out.printlnAnnotated(
            '$_coreImportPrefix.bool ${names.hasMethodName}() =>'
            ' \$_has(${field.index});',
            [
              NamedLocation(
                  name: names.hasMethodName,
                  fieldPathSegment: memberFieldPath,
                  start: '$_coreImportPrefix.bool '.length)
            ]);
      }
      _emitDeprecatedIf(field.isDeprecated, out);
      _emitOverrideIf(field.overridesClearMethod, out);
      _emitIndexAnnotation(field.number, out);
      out.printlnAnnotated(
          'void ${names.clearMethodName}() =>'
          ' clearField(${field.number});',
          [
            NamedLocation(
                name: names.clearMethodName,
                fieldPathSegment: memberFieldPath,
                start: 'void '.length)
          ]);
      if (field.baseType.isMessage) {
        _emitDeprecatedIf(field.isDeprecated, out);
        _emitIndexAnnotation(field.number, out);
        out.printlnAnnotated(
            '${fieldTypeString} ${names.ensureMethodName}() => '
            '\$_ensure(${field.index});',
            <NamedLocation>[
              NamedLocation(
                  name: names.ensureMethodName,
                  fieldPathSegment: memberFieldPath,
                  start: '${fieldTypeString} '.length)
            ]);
      }
    }
  }

  String _getterExpression(String fieldType, int index, String defaultExpr,
      bool isRepeated, bool isMapField) {
    if (isMapField) {
      return '\$_getMap($index)';
    }
    if (fieldType == '$_coreImportPrefix.String') {
      if (defaultExpr == '""' || defaultExpr == "''") {
        return '\$_getSZ($index)';
      }
      return '\$_getS($index, $defaultExpr)';
    }
    if (fieldType == '$_coreImportPrefix.bool') {
      if (defaultExpr == 'false') {
        return '\$_getBF($index)';
      }
      return '\$_getB($index, $defaultExpr)';
    }
    if (fieldType == '$_coreImportPrefix.int') {
      if (defaultExpr == '0') {
        return '\$_getIZ($index)';
      }
      return '\$_getI($index, $defaultExpr)';
    }
    if (fieldType == '$_fixnumImportPrefix.Int64' && defaultExpr == 'null') {
      return '\$_getI64($index)';
    }
    if (defaultExpr == 'null') {
      return isRepeated ? '\$_getList($index)' : '\$_getN($index)';
    }
    return '\$_get($index, $defaultExpr)';
  }

  void _emitDeprecatedIf(bool condition, IndentingWriter out) {
    if (condition) {
      out.println(
          '@$_coreImportPrefix.Deprecated(\'This field is deprecated.\')');
    }
  }

  void _emitOverrideIf(bool condition, IndentingWriter out) {
    if (condition) {
      out.println('@$_coreImportPrefix.override');
    }
  }

  void _emitIndexAnnotation(int index, IndentingWriter out) {
    out.println('@$_protobufImportPrefix.TagNumber($index)');
  }

  void generateEnums(IndentingWriter out) {
    for (var e in _enumGenerators) {
      e.generate(out);
    }

    for (var m in _messageGenerators) {
      m.generateEnums(out);
    }
  }

  /// Writes a Dart constant containing the JSON for the ProtoDescriptor.
  /// Also writes a separate constant for each nested message,
  /// to avoid duplication.
  void generateConstants(IndentingWriter out) {
    const nestedTypeTag = 3;
    const enumTypeTag = 4;
    assert(_descriptor.info_.fieldInfo[nestedTypeTag].name == "nestedType");
    assert(_descriptor.info_.fieldInfo[enumTypeTag].name == "enumType");

    var name = getJsonConstant(fileGen);
    var json = _descriptor.writeToJsonMap();
    var nestedTypeNames =
        _messageGenerators.map((m) => m.getJsonConstant(fileGen)).toList();
    var nestedEnumNames =
        _enumGenerators.map((e) => e.getJsonConstant(fileGen)).toList();

    out.addBlock("const $name = const {", "};", () {
      for (var key in json.keys) {
        out.print("'$key': ");
        if (key == "$nestedTypeTag") {
          // refer to message constants by name instead of repeating each value
          out.println("const [${nestedTypeNames.join(", ")}],");
          continue;
        } else if (key == "$enumTypeTag") {
          // refer to enum constants by name
          out.println("const [${nestedEnumNames.join(", ")}],");
          continue;
        }
        writeJsonConst(out, json[key]);
        out.println(",");
      }
    });
    out.println();

    for (var m in _messageGenerators) {
      m.generateConstants(out);
    }

    for (var e in _enumGenerators) {
      e.generateConstants(out);
    }
  }

  /// Returns the mixin for this message, or null if none.
  ///
  /// First searches [wellKnownMixins], then [declaredMixins],
  /// then internal mixins declared by [findMixin].
  PbMixin _getMixin(Map<String, PbMixin> declaredMixins, PbMixin defaultMixin) {
    var wellKnownMixin = wellKnownMixinForFullName(fullName);
    if (wellKnownMixin != null) return wellKnownMixin;
    if (!_descriptor.hasOptions() ||
        !_descriptor.options.hasExtension(Dart_options.mixin)) {
      return defaultMixin;
    }

    String name = _descriptor.options.getExtension(Dart_options.mixin);
    if (name.isEmpty) return null; // don't use any mixins (override default)
    var mixin = declaredMixins[name] ?? findMixin(name);
    if (mixin == null) {
      throw '${_descriptor.name} in ${_parent.fileGen.descriptor.name}: mixin "$name" not found';
    }
    return mixin;
  }
}
